/*
 * Copyright Aristool AG Switzerland
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */
package org.opprua.tidy.beans;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.opprua.tidy.conversion.ChangerBroker;
import org.opprua.tidy.util.StringUtil;

/**
 * TODO: add Java doc
 * 
 * Prepared for first commit: 15.07.2007
 * 
 * @author Lin Li
 */
public class BeanField {

  private static Map<String, Map<String, BeanField>> types = Collections
              .synchronizedMap(new TreeMap<String, Map<String, BeanField>>());
  private static Map<String, Map<Class<?>, Object[]>> paths = new TreeMap<String, Map<Class<?>, Object[]>>();

  private String name = "";
  private Class<?> type = null;
  private Field field = null;
  private Method getter = null;
  private Method setter = null;

  private BeanField() {
  }

  /**
   */
  public static synchronized Map<String, BeanField> getFields(Class<?> type) {

    if (types.containsKey(type.getName())) { return types.get(type.getName()); }

    Map<String, BeanField> fields = new TreeMap<String, BeanField>();
    types.put(type.getName(), fields);

    Field[] f = type.getFields();
    for (int i = 0; i < f.length; i++) {
      if (Modifier.isStatic(f[i].getModifiers())) {
        continue;
      }
      if (Modifier.isFinal(f[i].getModifiers())) {
        continue;
      }
      BeanField d = new BeanField();
      d.name = f[i].getName();
      d.field = f[i];
      d.type = f[i].getType();
      fields.put(f[i].getName(), d);
    }

    Method[] m = type.getMethods();

    for (int i = 0; i < m.length; i++) {
      if (Modifier.isStatic(m[i].getModifiers())
                  || isDeprecated(m[i].getAnnotations())) {
        continue;
      }
      String name = m[i].getName();
      if (!(name.startsWith("set") && name.length() > 3
                  && m[i].getParameterTypes().length == 1 && m[i]
                  .getReturnType() == void.class)) {
        continue;
      }

      name = StringUtil.depref(name, 3, true);
      Method setter = m[i];
      Class<?> rtype = setter.getParameterTypes()[0];
      Method getter = null;

      try {
        String gname = "get" + StringUtil.capFirst(name);
        getter = type.getMethod(gname, new Class<?>[0]);
      } catch (Throwable e) {
        if (rtype == boolean.class) {
          try {
            getter = type.getMethod(name, new Class<?>[0]);
          } catch (Throwable x) {}
        }
      }

      if (getter == null || rtype != getter.getReturnType()
                  || isDeprecated(getter.getAnnotations())) {
        continue;
      }

      BeanField d = (BeanField) fields.get(name);
      if (d == null) {
        d = new BeanField();
        d.name = name;
        fields.put(name, d);
      }
      d.field = null;
      d.type = rtype;
      d.getter = getter;
      d.setter = setter;
    }

    return fields;
  }

  /**
   */
  public String getName() {
    return name;
  }

  /**
   */
  @SuppressWarnings("unchecked")
  public Object get(Class<?> type, Object o) {
    try {
      if (type.isAssignableFrom(o.getClass())) { return o; }
      Object d = null;
      if (getter != null) {
        d = getter.invoke(o, new Object[0]);
      } else {
        d = field.get(o);
      }
      return ChangerBroker.convert(type, d);
    } catch (Throwable t) {
      throw new Error(
                  "Failed to get field " + name + " from a " + o.getClass(), t);
    }
  }

  /**
   */
  public Object get(Object o) {
    try {
      if (getter != null) {
        return getter.invoke(o, new Object[0]);
      } else {
        return field.get(o);
      }
    } catch (Throwable t) {
      throw new Error(
                  "Failed to get field " + name + " from a " + o.getClass(), t);
    }
  }

  /**
   */
  public void setNull(Object o) {
    if (getType().isPrimitive()) { throw new Error(
                "Can not set primitive field to null: " + getName()); }
    try {
      if (setter != null) {
        setter.invoke(o, new Object[] {
          null });
      } else {
        field.set(o, null);
      }
    } catch (Exception e) {
      throw new Error("Can not set field to null: " + getName());
    }
  }

  /**
   */
  public void set(Object o, Object d) {
    if (o == null || d == null) { return; }
    Object data = ChangerBroker.convert(type, d);
    try {
      if (data == null) {
        throw new Error("Conversion failed from " + d);
      } else if (data instanceof String) {
        data = ((String) data).trim();
      }
      if (setter != null) {
        setter.invoke(o, new Object[] {
          data });
      } else {
        field.set(o, data);
      }
    } catch (Throwable t) {
      throw new Error("Failed to set field " + name + " to a " + o.getClass(),
                  t);
    }
  }

  /**
   */
  public Class<?> getType() {
    return type;
  }

  static void set(Object o, Object[] path, Object value) {
    Object c = o;
    for (int i = 0; i < path.length - 1; i++) {
      c = ((BeanField) path[i]).get(c);
    }
    Object p = path[path.length - 1];
    if (p instanceof BeanField) {
      ((BeanField) p).set(c, value);
    } else {
      if (path.length == 1) {
        throw new Error("Can not set value with path " + p + " on a "
                    + c.getClass());
      } else {
        BeanHandler.set(o, (String) p, c);
      }
    }
  }

  static Object get(Object o, Object[] path) {
    if (path.length == 0) { return null; }
    Object c = o;
    for (int i = 0; i < path.length - 1; i++) {
      c = ((BeanField) path[i]).get(c);
    }
    Object p = path[path.length - 1];
    if (p instanceof BeanField) {
      return ((BeanField) p).get(c);
    } else if (path.length == 1) {
      throw new Error("Can not get field " + p + " from a " + c.getClass());
    } else {
      return BeanHandler.get(c, (String) p);
    }
  }

  static synchronized Object[] getPath(Class<?> t, String path) {

    Map<Class<?>, Object[]> types = paths.get(path);
    if (types == null) {
      types = new HashMap<Class<?>, Object[]>();
      paths.put(path, types);
    }
    Object[] res = types.get(t);
    if (res != null) { return res; }

    List<Object> p = new ArrayList<Object>();

    String segment = path.trim();
    if (segment.length() == 0) {
      new Error("Path empty for path finding " + t);
    }

    Class<?> type = t;
    while (segment.length() > 0) {
      String name = null;
      int sep = segment.indexOf(".");
      if (sep > 0) {
        name = segment.substring(0, sep).trim();
        segment = segment.substring(sep + 1).trim();
      } else {
        name = segment;
        segment = "";
      }

      Map<String, BeanField> m = getFields(type);
      if (m.containsKey(name)) {
        BeanField g = m.get(name);
        p.add(g);
        type = g.getType();
      } else if (segment.length() == 0) {
        p.add(name);
      } else {
        p.add(name + "." + segment);
        segment = "";
      }
    }

    res = p.toArray();
    types.put(t, res);
    return res;
  }

  public static boolean isDeprecated(Annotation[] notes) {
    for (int i = 0; i < notes.length; i++) {
      if (notes[i].toString().toLowerCase().indexOf("deprecated") > -1)
        return true;
    }
    return false;
  }
}